/**
 * BibSonomy-Webapp - The web application for BibSonomy.
 *
 * Copyright (C) 2006 - 2016 Knowledge & Data Engineering Group,
 *                               University of Kassel, Germany
 *                               http://www.kde.cs.uni-kassel.de/
 *                           Data Mining and Information Retrieval Group,
 *                               University of Würzburg, Germany
 *                               http://www.is.informatik.uni-wuerzburg.de/en/dmir/
 *                           L3S Research Center,
 *                               Leibniz University Hannover, Germany
 *                               http://www.l3s.de/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.bibsonomy.webapp.validation;

import static org.bibsonomy.util.ValidationUtils.present;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import org.bibsonomy.bibtex.parser.PostBibTeXParser;
import org.bibsonomy.bibtex.parser.SimpleBibTeXParser;
import org.bibsonomy.model.BibTex;
import org.bibsonomy.model.util.BibTexUtils;
import org.bibsonomy.util.UrlUtils;
import org.bibsonomy.webapp.util.Validator;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;

import bibtex.expansions.CrossReferenceExpansionException;
import bibtex.expansions.ExpansionException;
import bibtex.expansions.PersonListParserException;
import bibtex.parser.ParseException;

/**
 * @author fba
 * @author dzo
 */
public class PublicationValidator implements Validator<BibTex> {

	private static final String PARSE_ERROR_MESSAGE_KEY = "error.parse.bibtex.failed";
	private static final String DEFAULT_PARSE_ERROR_MESSAGE = "Error parsing your post:\n\n{0}\n\nMessage was: {1}";

	@Override
	public boolean supports(final Class<?> clazz) {
		return BibTex.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(final Object obj, final Errors errors) {
		Assert.notNull(obj);

		if (obj instanceof BibTex) {
			final BibTex bibtex = (BibTex) obj;

			/*
			 * clean url
			 * FIXME: a validator MUST NOT modify objects!
			 */
			bibtex.setUrl(UrlUtils.cleanUrl(bibtex.getUrl()));

			/*
			 * check url
			 */
			//			final String url = resource.getUrl();
			//			if (url == null || url.equals("http://") || url.startsWith(UrlUtils.BROKEN_URL)) {
			//				errors.rejectValue("post.resource.url", "error.field.valid.url");
			//			}

			/*
			 * if no BibTeX key is available, we generate one
			 * FIXME: a validator MUST NOT modify objects!
			 */
			if (!present(bibtex.getBibtexKey())) {
				bibtex.setBibtexKey(BibTexUtils.generateBibtexKey(bibtex));
			}
			if (!present(bibtex.getBibtexKey()) || containsWhiteSpace(bibtex.getBibtexKey())) {
				errors.rejectValue("bibtexKey", "error.field.valid.bibtexKey");
			}

			/*
			 * initialize parser
			 * 
			 * XXX: if the parser is thread safe, we can use a single instance for
			 * several calls.
			 */
			final PostBibTeXParser parser = new PostBibTeXParser();

			/*
			 * test misc field using the BibTeXParser
			 */
			final String misc = bibtex.getMisc();
			if (present(misc)) {
				/*
				 * parse a bibtex string only with attributes of the misc field 
				 */
				try {
					parser.parseBibTeX("@misc{id,\n" + misc + "\n}");
				} catch (ParseException ex) {
					errors.rejectValue("misc", "error.field.valid.misc");
					/*
					 * stop parsing remaining entry - would fail anyway.
					 */
					return;
				} catch (IOException ex) {
					errors.rejectValue("misc", "error.field.valid.misc");
					/*
					 * stop parsing remaining entry - would fail anyway.
					 */
					return;
				}
			}

			/*
			 * test validity using the BibTeXParser
			 */
			final String bibTexAsString = BibTexUtils.toBibtexString(bibtex);
			try {
				parser.parseBibTeX(bibTexAsString);
			} catch (ParseException ex) {
				/*
				 * parsing failed
				 */
				errors.reject(PARSE_ERROR_MESSAGE_KEY, new Object[]{bibTexAsString, ex.getMessage()}, DEFAULT_PARSE_ERROR_MESSAGE);
			} catch (IOException ex) {
				/*
				 * parsing failed
				 */
				errors.reject(PARSE_ERROR_MESSAGE_KEY, new Object[]{bibTexAsString, ex.getMessage()}, DEFAULT_PARSE_ERROR_MESSAGE);
			}
			/*
			 * add parser warnings to errors
			 */
			handleParserWarnings(errors, parser, bibTexAsString, "author");

			/*
			 * We add the "simple" checks after replacing the publication with
			 * the parsed one to ensure we catch empty fields.
			 *  
			 * (reason: the BibTeX parser does not recognize certain broken 
			 * author names and then removes them, e.g., "Foo, Bar," resulting
			 * in no errors but an empty author field.)  
			 */

			/*
			 * entrytype
			 */
			if (!present(bibtex.getEntrytype()) || containsWhiteSpace(bibtex.getEntrytype())) {
				errors.rejectValue("entrytype", "error.field.valid.entrytype");
			}

			/*
			 * year
			 */
			if (!present(bibtex.getYear())) {
				errors.rejectValue("year", "error.field.valid.year");
			}
			/*
			 * author/editor
			 * (we don't add the error if there is already an error added - it 
			 * might be a more detailed error message from the parser from
			 * handleParserWarnings())
			 */
			if (!present(bibtex.getAuthor()) && !present(bibtex.getEditor())) { 
				errors.rejectValue("author", "error.field.valid.authorOrEditor");
				// one error is enough
				//errors.rejectValue("post.resource.editor", "error.field.valid.authorOrEditor");
			}

		}
	}

	/**
	 * Checks the parser's warnings for serious errors (e.g., wrong person names)
	 * and adds them to the errors list.
	 * 
	 * @param errors
	 * @param parser
	 * @param bibTexAsString
	 * @param authorFieldName - if given, person name parsing errors are 
	 * added to this field. If several posts are parsed, we currently can't assign
	 * the errors to the correct post. In this case, set this value to <code>null</code>.  
	 * to this field. The errors are then added as global errors.
	 */
	public static void handleParserWarnings(final Errors errors, final SimpleBibTeXParser parser, final String bibTexAsString, final String authorFieldName) {
		final List<ExpansionException> warnings = parser.getWarnings();
		final List<String> warningsToError = new LinkedList<String>();
		if (present(warnings)) {
			for (final ExpansionException warning : warnings) {
				final Class<? extends ExpansionException> clazz = warning.getClass();
				if (PersonListParserException.class.equals(clazz)) {
					/*
					 * special handling for person name parsing errors, e.g.,  
					 * "Name ends with comma: 'Foo, Bar,' - in 'foo'"
					 */
					if (present(authorFieldName)) {
						/*
						 * FIXME: we don't know whether to reject author or editor.
						 * So we pick the author = best guess. Not a good idea but
						 * my quick solution for today. :-( 
						 */
						errors.rejectValue(authorFieldName, "error.field.valid.authorOrEditor.parseError", new Object[]{warning.getMessage()}, "The author or editor field caused the following parse error: {0}");
					} else {
						/*
						 * We add the errors as global errors.  
						 */
						warningsToError.add(warning.getMessage());
					}
				} else if (CrossReferenceExpansionException.class.equals(clazz)) {
					/*
					 * new since 2014-11-14: we ignore crossref expansion errors
					 * (TODO: show them as warnings instead)
					 */
				} else {
					/*
					 * we add all other warnings as errors
					 */
					warningsToError.add(warning.getMessage());
				}
			}
		}
		if (present(warningsToError)) {
			errors.reject(PARSE_ERROR_MESSAGE_KEY, new Object[]{bibTexAsString, toString(warningsToError)}, DEFAULT_PARSE_ERROR_MESSAGE);
		}
	}


	private static String toString(final List<String> list) {
		final StringBuffer buf = new StringBuffer();
		for (final String item : list) {
			buf.append(item).append("\n");
		}
		return buf.toString();
	}

	private static boolean containsWhiteSpace(final String s) {
		return s.matches("\\s");
	}
}
